package message;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import main.util.RouterConfig;

public class Router {
	
	private static final long UPDATE_INTERVAL = 5;
	private static final String MESSAGE_SEPARATOR = ";";
	
	private int router_port;
	private String router_ip;
	private int[] neighboursId;
	
	private Map<Integer, UDPConnection> connections;
	
	private Node node;
	private DatagramSocket serverSocket;

	public Router(int router_id, int router_port, String router_ip,
			int[] neighboursId, int[] neighboursDistance, String[] neighboursIp, int[] neighboursPort, int routersCount) {
		this.router_port = router_port;
		this.router_ip = router_ip;
		this.neighboursId = neighboursId;
		
		node = new Node(router_id, neighboursId, neighboursDistance, routersCount);
		initConnections(neighboursId, neighboursIp, neighboursPort);
	}
	
	public Router(RouterConfig config, int routersCount){
		this(config.getId(), config.getPort(), config.getIp(), config.getNeighbours(), config.getDistances(), config.getIps(), config.getNeighbourPorts(), routersCount);
	}
	
	private void initConnections(int[] neighboursId, String[] neighboursIp, int[] neighboursPort) {
		connections = new HashMap<Integer, UDPConnection>();
		
		for (int i = 1; i < neighboursId.length; i++)
			connections.put(neighboursId[i], new UDPConnection(neighboursIp[i], neighboursPort[i]));  
	}

	public void start() {
		
		startSendDistanceVector();
		listenNeighbourUpdates();	
	} // start()

	private void startSendDistanceVector() {
		
		ScheduledExecutorService newSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
		
		newSingleThreadExecutor.scheduleWithFixedDelay(new Runnable(){

			public void run() {
				checkNeighboursFailure();
				
				List<DistanceEstimation> distanceVector = node.getDistanceVector();
				
				for (int i = 1; i < neighboursId.length; i++)
					try {
						sendDistanceVector(distanceVector, neighboursId[i]);
					} catch (IOException e) {
						e.printStackTrace();
					}
				
				node.cleanDistanceVector();
			}

			private void checkNeighboursFailure() {
				for (Integer neighbourId : connections.keySet()) {
					UDPConnection connection = connections.get(neighbourId);
					boolean failure = connection.checkTimeOut();
					if (failure)
						node.setNeighbourFailure(neighbourId);
				}
			}
			
		}, 0, UPDATE_INTERVAL, TimeUnit.SECONDS);
	}

	private void listenNeighbourUpdates() {
		
		while (true) {
			
			//receive message
			try {
				
				String receiveMessage = receiveMessage();
				
				System.out.println(receiveMessage);
				
				int senderId = Integer.parseInt(getMessageSender(receiveMessage));
				UDPConnection connection = connections.get(senderId);
				
				if (connection.checkTimeOut()) {
					sendDistanceVector(node.getBestEstimation(), senderId);
				}
				
				connection.updateLastResponse();
				node.receiveDistanceVector(getDistanceVector(receiveMessage));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	private DatagramSocket getServerSocket() throws SocketException, UnknownHostException {
		if (serverSocket == null) {
			serverSocket = new DatagramSocket(router_port, getServerAddress());
		}
		return serverSocket;
	}
	
	public String receiveMessage() throws IOException {
		
		byte[] receiveData = new byte[UDPConnection.MTU];
		
		DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
		getServerSocket().receive(receivePacket);
		
		List<Byte> receiveDataList = new LinkedList<Byte>();
		
		for (int i = 0; i < UDPConnection.MTU; i++) {
			if (receiveData[i] == 0) {
				break;
			}
			receiveDataList.add(receiveData[i]);
		}
		
		byte[] result = new byte[receiveDataList.size()];
		
		for (int i = 0; i < result.length; i++) {
			result[i] = receiveDataList.get(i);
		}
			
		return new String(result);
	}

	private InetAddress getServerAddress() throws UnknownHostException {
		return InetAddress.getByName(router_ip);
	}

	private void sendDistanceVector(List<DistanceEstimation> distanceVector, int neighbourId) throws IOException {
		List<DistanceEstimation> filteredDistanceVector = filterDistanceVector(distanceVector, neighbourId);
		connections.get(neighbourId).sendMessage(buildDistanceVectorMessage(filteredDistanceVector, neighbourId));
	}

	private List<DistanceEstimation> filterDistanceVector(List<DistanceEstimation> distanceVector, int neighbourId) {
		List<DistanceEstimation> filteredDistanceVector = new LinkedList<DistanceEstimation>();
		
		for (DistanceEstimation distanceEstimation : distanceVector) {
			
			int destinationId = distanceEstimation.getDestinationId();
			if ((checkNeighbour(destinationId) && connections.get(destinationId).checkTimeOut())  || neighbourId == node.getMinNeighbourId(destinationId)) {
				distanceEstimation.setDistance(Node.MAX_VALUE);
			}
			
			filteredDistanceVector.add(distanceEstimation);
		}
		return filteredDistanceVector;
	}

	private boolean checkNeighbour(int destinationId) {
		return node.isNeighbour(destinationId);
	}

	private String buildDistanceVectorMessage(List<DistanceEstimation> distanceVector, int neighbourId) {
		String message = "Sender" + MESSAGE_SEPARATOR + node.getId() + MESSAGE_SEPARATOR;
		
		for (DistanceEstimation distanceEstimation : distanceVector) {
			message += distanceEstimation.toString() + MESSAGE_SEPARATOR;
		}
		return message;
	}
	
	private String getMessageSender(String message) {
		String[] split = message.split(MESSAGE_SEPARATOR);
		return split[1];
	}

	private List<DistanceEstimation> getDistanceVector(String message) {
		List<DistanceEstimation> vector = new LinkedList<DistanceEstimation>();
		String[] tokens = message.split(MESSAGE_SEPARATOR);
		
		for (int i = 2; i < tokens.length; i++)
			vector.add(new DistanceEstimation(tokens[i]));
		
		return vector;
	}
}